﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using CannedHeater.Model;
using CannedHeater.ViewModel.DataContainers;

namespace CannedHeater.ViewModel
{
    /// <summary>
    /// ViewModel for the ControlView
    /// </summary>
    public class ControlViewModel : INotifyPropertyChanged
    {
        #region Events
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Properties
        private DeviceRemoteControl _controller;
        /// <summary>
        /// The object actually communicating with, and controlling the netduino device
        /// </summary>
        public DeviceRemoteControl Controller
        {
            get { return _controller; }
            private set
            {
                _controller = value;

                // Update other controller related properties as well
                Connected = _controller.Connected;
                StatusMsg = _controller.StatusMsg;
                SensorData.AirFlow = _controller.SensorData.AirFlow;
                SensorData.IndoorTemp = _controller.SensorData.IndoorTemp;
                SensorData.OutdoorTemp = _controller.SensorData.OutdoorTemp;
                SensorData.SolarHeaterTemp = _controller.SensorData.SolarHeaterTemp;
                _controller.SensorData.PropertyChanged += new PropertyChangedEventHandler(SensorData_PropertyChanged);
                _controller.PropertyChanged += new PropertyChangedEventHandler(_controller_PropertyChanged);

                RaisePropertyChanged("Controller");
            }
        }

        #region Extra properties from Controller for bindings
        private bool _connected = false;
        /// <summary>
        /// Whether or not the app is currently connected to the netduino device
        /// </summary>
        public bool Connected
        {
            get { return _connected;  }
            private set
            {
                _connected = value;
                RaisePropertyChanged("Connected");
            }
        }

        private string _statusMsg;
        /// <summary>
        /// Latest status message regarding the connection between the app and netduino device
        /// </summary>
        public string StatusMsg
        {
            get { return _statusMsg;  }
            private set
            {
                _statusMsg = value;
                RaisePropertyChanged("StatusMsg");
            }
        }

        private StatusPanelData _statusPanelData = new StatusPanelData();
        /// <summary>
        /// The latest sensor data from the netduino device
        /// </summary>
        public StatusPanelData SensorData
        {
            get { return _statusPanelData;  }
        }
        #endregion

        private ModeType _controlMode = ModeType.Auto;
        /// <summary>
        /// The control mode to be used for commands to netduino device
        /// </summary>
        public ModeType ControlMode
        {
            get { return _controlMode; }
            set
            {
                if (value != ModeType.Unknown) // Ignore unknown (binding targets-to-source)
                {
                    _controlMode = value;
                    RaisePropertyChanged("ControlMode");
                }
            }
        }

        private int _controlSpeed = 0; // If input can be parsed as int, store it as such
        private string _controlSpeedTxt = "0";
        /// <summary>
        /// The manual speed for the netduino device - represented as text (validate input from binding of textbox)
        /// </summary>
        public string ControlSpeedTxt
        {
            get { return _controlSpeedTxt; }
            set
            {
                _controlSpeedTxt = value; // Always save latest input
                try
                {
                    _controlSpeed = int.Parse(value); // Try to convert to int
                    ValidateSpeed(value); // Validate the data
                    ControlSpeedValid = true; // If passed validation
                }catch
                {
                    ControlSpeedValid = false;
                }
                RaisePropertyChanged("ControlSpeedTxt");
            }
        }

        private bool _controlSpeedValid = true;
        /// <summary>
        /// Whether or not the text input is a valid control speed
        /// </summary>
        public bool ControlSpeedValid
        {
            get { return _controlSpeedValid; }
            set
            {
                _controlSpeedValid = value;
                RaisePropertyChanged("ControlSpeedValid");
            }
        }

        private double _controlTemp = 0; // If input can be parsed as double, store it as such
        private string _controlTempTxt = "0";
        /// <summary>
        /// The temp to use for <see cref="ModeType.Temp"/> - represented as text (validate input from binding of textbox)
        /// </summary>
        public string ControlTempTxt
        {
            // Similar to ControlSpeedTxt

            get { return _controlTempTxt;  }
            set
            {
                _controlTempTxt = value;
                try
                {
                    _controlTemp = double.Parse(value.Replace(',','.'));
                    ValidateTemp(value);
                    ControlTempValid = true;
                }
                catch
                {
                    ControlTempValid = false;
                }
                RaisePropertyChanged("ControlTempTxt");
            }
        }

        private bool _controlTempValid = true;
        /// <summary>
        /// Whether or not the text input is a valid temperature
        /// </summary>
        public bool ControlTempValid
        {
            get { return _controlTempValid; }
            set
            {
                _controlTempValid = value;
                RaisePropertyChanged("ControlTempValid");
            }
        }

        private int _controlTime = 1; // If input can be parsed as int, store it as such
        private string _controlTimeTxt = "1";
        /// <summary>
        /// The time to use for <see cref="ModeType.Time"/> - represented as text (validate input from binding of textbox)
        /// </summary>
        public string ControlTimeTxt
        {
            // Similar to ControlSpeedTxt

            get { return _controlTimeTxt; }
            set
            {
                _controlTimeTxt = value;
                try
                {
                    _controlTime = int.Parse(value);
                    ValidateTime(value);
                    ControlTimeValid = true;
                }
                catch
                {
                    ControlTimeValid = false;
                }
                RaisePropertyChanged("ControlTimeTxt");
            }
        }

        private bool _controlTimeValid = true;
        /// <summary>
        /// Whether or not the text input is a valid amount of time
        /// </summary>
        public bool ControlTimeValid
        {
            get { return _controlTimeValid; }
            set
            {
                _controlTimeValid = value;
                RaisePropertyChanged("ControlTimeValid");
            }
        }
        #endregion

        #region Controller properties changes
        /// <summary>
        /// Handles changes in Controller properties, copies data to viewmodel
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void _controller_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "Connected":
                    Connected = Controller.Connected;
                    break;
                case "StatusMsg":
                    StatusMsg = Controller.StatusMsg;
                    break;
                case "SensorData":
                    SensorData.AirFlow = Controller.SensorData.AirFlow;
                    SensorData.IndoorTemp = Controller.SensorData.IndoorTemp;
                    SensorData.OutdoorTemp = Controller.SensorData.OutdoorTemp;
                    SensorData.SolarHeaterTemp = Controller.SensorData.SolarHeaterTemp;
                    break;
            }
        }

        /// <summary>
        /// Handles changes in Controller property SensorData, copies data to viewmodel
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SensorData_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch ( e.PropertyName )
            {
                case "AirFlow":
                    SensorData.AirFlow = Controller.SensorData.AirFlow;
                    break;
                case "IndoorTemp":
                    SensorData.IndoorTemp = Controller.SensorData.IndoorTemp;
                    break;
                case "OutdoorTemp":
                    SensorData.OutdoorTemp = Controller.SensorData.OutdoorTemp;
                    break;
                case "SolarHeaterTemp":
                    SensorData.SolarHeaterTemp = Controller.SensorData.SolarHeaterTemp;
                    break;
            }
        }
        #endregion
 

        #region Validation
        /// <summary>
        /// Checks if a string is a valid speed
        /// </summary>
        /// <param name="str">Speed step as string</param>
        public void ValidateSpeed(string str)
        {
            int speed;
            bool res = int.TryParse(str, out speed);
            if (!res) throw new Exception("Speed must be an integer (step)");
            else ValidateSpeed(speed);
        }

        /// <summary>
        /// Checks if a speed is valid
        /// </summary>
        /// <param name="speed">Speed</param>
        private void ValidateSpeed(int speed)
        {
            if (speed < 0 || speed > 10)
                throw new Exception("Speed must be within range 0-10");
        }

        /// <summary>
        /// Checks if a string is a valid time
        /// </summary>
        /// <param name="str">Time (mins) as string</param>
        public void ValidateTime(string str)
        {
            int time;
            bool res = int.TryParse(str, out time);
            if (!res) throw new Exception("Time must be an integer (minutes)");
            else ValidateTime(time);
        }

        /// <summary>
        /// Checks if a time is valid
        /// </summary>
        /// <param name="time">Time (mins)</param>
        private void ValidateTime(int time)
        {
            if (time <= 0)
                throw new Exception("Time must be > 0");
        }

        /// <summary>
        /// Checks if a string is a valid temperature
        /// </summary>
        /// <param name="str">Temperature as string</param>
        public void ValidateTemp(string str)
        {
            double temp;
            bool res = double.TryParse(str, out temp);
            if (!res) throw new Exception("Temp must be a decimal number");
            else ValidateTemp(temp);
        }

        /// <summary>
        /// Checks if a temperature is valid
        /// </summary>
        /// <param name="temp">Temperature</param>
        private void ValidateTemp(double temp)
        {
            // All valid
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Constructs a new ControlViewModel
        /// </summary>
        public ControlViewModel()
        {
            // Create instance of device remote control ("prepare communication")
            Controller = new DeviceRemoteControl(Settings.GlobalSettings.NETDUINO_DEVICE_IP,
                                                  Settings.GlobalSettings.NETDUINO_DEVICE_PORT);
        }
        #endregion

        /// <summary>
        /// Connects to netduino device, or, if already connected, sends a command
        /// </summary>
        public void ConnectOrExecCommand()
        {
            if (!Controller.Connected) Controller.Connect();
            else ExecCommand();
        }

        /// <summary>
        /// Sends a command to the netduino device (through the <see cref="Controller"/>)
        /// </summary>
        private void ExecCommand()
        {
            // Send new command to Controller, if all input data is valid
            // Otherwise, show an error

            if ( ControlMode == ModeType.Unknown )
                MessageBox.Show("No command type selected", "Error", MessageBoxButton.OK);
            else if ( ControlMode == ModeType.Auto )
                Controller.ExecCommand(new DeviceRemoteControlCommand() { CommandType = DeviceRemoteControlCommandType.Auto });
            else if ( ControlMode == ModeType.Manual )
            {
                if ( ControlSpeedValid )
                    Controller.ExecCommand(new DeviceRemoteControlCommand() { CommandType = DeviceRemoteControlCommandType.Speed, Speed = _controlSpeed });
                else
                {
                    // Some error, just try
                    try
                    {
                        ValidateSpeed(_controlSpeedTxt);
                    }catch(Exception e)
                    {
                        MessageBox.Show(e.Message, "Input error", MessageBoxButton.OK);
                    }
                }
            }else if ( ControlMode == ModeType.Time)
            {
                if (ControlSpeedValid && ControlTimeValid)
                    Controller.ExecCommand(new DeviceRemoteControlCommand() { CommandType = DeviceRemoteControlCommandType.SpeedTime, Speed = _controlSpeed, Time = _controlTime });
                else
                {
                    // Some error, just try
                    try
                    {
                        ValidateSpeed(_controlSpeedTxt);
                        ValidateTime(_controlTimeTxt);
                    }
                    catch (Exception e)
                    {
                        MessageBox.Show(e.Message, "Input error", MessageBoxButton.OK);
                    }
                }
            }else if ( ControlMode == ModeType.Temp)
            {
                if (ControlSpeedValid && ControlTempValid)
                    Controller.ExecCommand(new DeviceRemoteControlCommand() { CommandType = DeviceRemoteControlCommandType.SpeedTemp, Speed = _controlSpeed, Temp = _controlTemp });
                else
                {
                    // Some error, just try
                    try
                    {
                        ValidateSpeed(_controlSpeedTxt);
                        ValidateTemp(_controlTempTxt);
                    }
                    catch (Exception e)
                    {
                        MessageBox.Show(e.Message, "Input error", MessageBoxButton.OK);
                    }
                }
            }
        }

        #region Tombstone
        /// <summary>
        /// Saves input data in tombstone
        /// </summary>
        /// <param name="state">The page state used for tombstoning</param>
        public void Tombstone(IDictionary<string, object> state)
        {
            // Save input data in current state
            SetDictionaryEntry("CVM_SensorData_IndoorTemp", Controller.SensorData.IndoorTemp, state);
            SetDictionaryEntry("CVM_SensorData_OutdoorTemp", Controller.SensorData.OutdoorTemp, state);
            SetDictionaryEntry("CVM_SensorData_SolarHeaterTemp", Controller.SensorData.SolarHeaterTemp, state);
            SetDictionaryEntry("CVM_SensorData_AirFlow", Controller.SensorData.AirFlow, state);
            SetDictionaryEntry("CVM_Connected", Controller.Connected, state);
            SetDictionaryEntry("CVM_StatusMsg", Controller.StatusMsg, state);
            SetDictionaryEntry("CVM_ControlMode", ControlMode, state);
            SetDictionaryEntry("CVM_ControlSpeedTxt", ControlSpeedTxt, state);
            SetDictionaryEntry("CVM_ControlTempTxt", ControlTempTxt, state);
            SetDictionaryEntry("CVM_ControlTimeTxt", ControlTimeTxt, state);
        }

        /// <summary>
        /// Loads input data from tombstone
        /// </summary>
        /// <param name="state">The page state used for tombstoning</param>
        public void LoadTombstone(IDictionary<string, object> state)
        {
            if (state.Count == 0) return; // No need to load if no data stored

            // Load data from state, if not found, use current value of the properties (fallback)

            double indoorTemp = GetSettingOnce("CVM_SensorData_IndoorTemp", Controller.SensorData.IndoorTemp, state);
            double outdoorTemp = GetSettingOnce("CVM_SensorData_OutdoorTemp", Controller.SensorData.OutdoorTemp, state);
            double solarHeaterTemp = GetSettingOnce("CVM_SensorData_SolarHeaterTemp", Controller.SensorData.SolarHeaterTemp, state);
            double airFlow = Controller.SensorData.AirFlow = GetSettingOnce("CVM_SensorData_AirFlow", Controller.SensorData.AirFlow, state);
            bool connected = GetSettingOnce("CVM_Connected", Controller.Connected, state);
            string statusMsg = GetSettingOnce("CVM_StatusMsg", Controller.StatusMsg, state);

            // Restore the device remote controller from the tombstoned data

            DeviceSensorData sensorData = new DeviceSensorData();
            sensorData.IndoorTemp = indoorTemp;
            sensorData.OutdoorTemp = outdoorTemp;
            sensorData.SolarHeaterTemp = solarHeaterTemp;
            sensorData.AirFlow = airFlow;
            Controller = DeviceRemoteControl.RestoreController(Settings.GlobalSettings.NETDUINO_DEVICE_IP,
                                                               Settings.GlobalSettings.NETDUINO_DEVICE_PORT,
                                                               connected, statusMsg, sensorData);

            // Restore properties

            ControlMode = GetSettingOnce("CVM_ControlMode", ControlMode, state);
            ControlSpeedTxt = GetSettingOnce("CVM_ControlSpeedTxt", ControlSpeedTxt, state);
            ControlTempTxt = GetSettingOnce("CVM_ControlTempTxt", ControlTempTxt, state);
            ControlTimeTxt = GetSettingOnce("CVM_ControlTimeTxt", ControlTimeTxt, state);
        }

        /// <summary>
        /// Sets entry <paramref name="key"/> in <paramref name="dict"/> to <paramref name="val"/>
        /// </summary>
        /// <param name="key">Key in dictionary</param>
        /// <param name="val">Value to store</param>
        /// <param name="dict">Dictionary to store value in</param>
        private void SetDictionaryEntry(string key, object val, IDictionary<string, object> dict)
        {
            if (dict.ContainsKey(key))
                dict[key] = val;
            else
                dict.Add(key, val);
        }

        /// <summary>
        /// Gets the value from <paramref name="dict"/> identified by <paramref name="key"/>, and removes the entry in <paramref name="dict"/>
        /// </summary>
        /// <param name="key">Key in dictionary</param>
        /// <param name="fallback">Value to use if no value for key exists in <paramref name="dict"/></param>
        /// <param name="dict"></param>
        /// <returns>
        /// Value from <paramref name="dict"/> identified by <paramref name="key"/>.
        /// If not exists, returns <paramref name="fallback"/>
        /// </returns>
        private ModeType GetSettingOnce(string key, ModeType fallback, IDictionary<string, object> dict)
        {
            ModeType res = fallback;
            if (dict.ContainsKey(key))
                res = (ModeType)dict[key];
            dict.Remove(key); // By removing after get, data can only be loaded right after activation from tombstoned state
            return res;
        }

        /// <summary>
        /// Gets the value from <paramref name="dict"/> identified by <paramref name="key"/>, and removes the entry in <paramref name="dict"/>
        /// </summary>
        /// <param name="key">Key in dictionary</param>
        /// <param name="fallback">Value to use if no value for key exists in <paramref name="dict"/></param>
        /// <param name="dict"></param>
        /// <returns>
        /// Value from <paramref name="dict"/> identified by <paramref name="key"/>.
        /// If not exists, returns <paramref name="fallback"/>
        /// </returns>
        private double GetSettingOnce(string key, double fallback, IDictionary<string, object> dict)
        {
            double res = fallback;
            if (dict.ContainsKey(key))
                res = (double)dict[key];
            dict.Remove(key); // By removing after get, data can only be loaded right after activation from tombstoned state
            return res;
        }

        /// <summary>
        /// Gets the value from <paramref name="dict"/> identified by <paramref name="key"/>, and removes the entry in <paramref name="dict"/>
        /// </summary>
        /// <param name="key">Key in dictionary</param>
        /// <param name="fallback">Value to use if no value for key exists in <paramref name="dict"/></param>
        /// <param name="dict"></param>
        /// <returns>
        /// Value from <paramref name="dict"/> identified by <paramref name="key"/>.
        /// If not exists, returns <paramref name="fallback"/>
        /// </returns>
        private bool GetSettingOnce(string key, bool fallback, IDictionary<string, object> dict)
        {
            bool res = fallback;
            if (dict.ContainsKey(key))
                res = (bool)dict[key];
            dict.Remove(key); // By removing after get, data can only be loaded right after activation from tombstoned state
            return res;
        }

        /// <summary>
        /// Gets the value from <paramref name="dict"/> identified by <paramref name="key"/>, and removes the entry in <paramref name="dict"/>
        /// </summary>
        /// <param name="key">Key in dictionary</param>
        /// <param name="fallback">Value to use if no value for key exists in <paramref name="dict"/></param>
        /// <param name="dict"></param>
        /// <returns>
        /// Value from <paramref name="dict"/> identified by <paramref name="key"/>.
        /// If not exists, returns <paramref name="fallback"/>
        /// </returns>
        private string GetSettingOnce(string key, string fallback, IDictionary<string, object> dict)
        {
            string res = fallback;
            if (dict.ContainsKey(key))
                res = (string)dict[key];
            dict.Remove(key); // By removing after get, data can only be loaded right after activation from tombstoned state
            return res;
        }


        #endregion


        #region Helpers
        public void RaisePropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
        #endregion
    }

    /// <summary>
    /// The possible control modes of the netduino
    /// </summary>
    public enum ModeType
    {
        /// <summary>
        /// Automatic
        /// </summary>
        Auto,
        /// <summary>
        /// Manual (constant speed)
        /// </summary>
        Manual,
        /// <summary>
        /// Manual (constant speed until reached indoor temperature)
        /// </summary>
        Temp,
        /// <summary>
        /// Manual (constant speed for some specified time)
        /// </summary>
        Time,
        /// <summary>
        /// Unknown control mode, ignored
        /// </summary>
        Unknown
    }
}
